﻿using System;

using System.Collections.Generic;
using System.Text;
using System.Drawing;
using Silvermoon.OpenGL.Native;

namespace Silvermoon.OpenGL
{
    /// <summary>
    /// Renders strings using <see cref="FontSprite"/>. Instead of creating a texture for each string, only one texture (from FontSprite) is used
    /// as reference and vertex and texture pointers are used to render the string. TextureString also caches previousely created vertex/texture pointers as long
    /// as any pending property (e.g. <paramref name="Text"/>) does not change.
    /// </summary>
    public class TextSprite : IDisposable
    {
        #region ctor/dtor

        public TextSprite(FontSprite font)
            : base()
        {
            this.Font = font;
        }

        public TextSprite()
            : base()
        {
        }

        public virtual void Dispose()
        {
            indexPtr = null;
        }

        #endregion
        #region fields

        float scaleFactor = 1F;

        private Size clipSize;
        private Size textSize;
        private FontSprite font;
        private int characterSpace;
        private string text = string.Empty;
        private short[] indexPtr;
        private short[] vertexPtr;
        private float[] fontPtr;
        private LineMode mode = LineMode.SingleLine;
        private Alignment alignment = Alignment.Near;
        private Alignment lineAlignment = Alignment.Near;

        #endregion
        #region Props

        public bool WordWrap { get { return (mode & LineMode.WordWrap) == LineMode.WordWrap; } }
        public bool SingleLine { get { return (mode & LineMode.WordWrap) == LineMode.SingleLine; } }
        public bool MultiLine { get { return (mode & LineMode.WordWrap) == LineMode.MultiLine; } }
        public bool EndEllipsis { get { return (mode & LineMode.EndEllipsis) != 0; } }

        public Alignment Alignment { get { return alignment; } set { if (alignment != value) alignment = value; InvalidateData(); } }
        public Alignment LineAlignment { get { return lineAlignment; } set { if (lineAlignment != value) { lineAlignment = value; InvalidateData(); } } }


        /// <summary>
        /// Gets or sets how to handle multiple lines.
        /// </summary>
        public LineMode LineMode
        {
            get { return mode; }
            set { if (value == LineMode.EndEllipsis) value = LineMode.EndEllipsis | LineMode.SingleLine; if (mode != value) { mode = value; InvalidateData(); } }
        }

        /// <summary>
        /// Gets or sets the factor how text is scaled. If ScaleFactor>1f, text gets larger, otherwise smaller.
        /// This enables TextSprite to use only one (large) TextFont to render text in different sizes instead of using different TextFont for each size with different underlying textures and
        /// thus saving graphic resources.
        /// </summary>
        public float ScaleFactor
        {
            get { return scaleFactor; }
            set { scaleFactor = value; }
        }

        public int MyProperty { get; set; }

        /// <summary>
        /// Gets or sets the <see cref="FontSpite"/> to use as reference to render each character refered to the specified <see cref="FontSprite"/>.
        /// </summary>
        public FontSprite Font
        {
            get { return font ?? FontSprite.Regular; }
            set
            {
                if (font != value)
                {
                    font = value;
                    InvalidateData();
                }
            }
        }

        /// <summary>
        /// Gets or sets the text to render.
        /// </summary>
        public string Text
        {
            get { return text ?? String.Empty; }
            set
            {
                if (text != value)
                {
                    text = value;
                    textSize = Size.Empty;
                    InvalidateData();
                }
            }
        }

        /// <summary>
        /// Gets the actual size of the string.
        /// </summary>
        public Size TextSize
        {
            get
            {
                if (textSize.IsEmpty) textSize = MeasureSize();
                return textSize;
            }
        }

        /// <summary>
        /// Gets or sets the maximum allowed size for the text. if set to 0/0, this value is ignored.
        /// </summary>
        public Size ClipSize
        {
            get { return clipSize; }
            set
            {
                if (clipSize != value)
                {
                    clipSize = value;
                    InvalidateData();
                }
            }
        }

        public Color Color { get; set; }

        /// <summary>
        /// Gets or sets the space between characters.
        /// </summary>
        public int CharacterSpace
        {
            get { return characterSpace; }
            set
            {
                if (characterSpace != value)
                {
                    characterSpace = value;
                    InvalidateData();
                }
            }
        }

        #endregion
        #region Methods

        /// <summary>
        /// Invalidates the calculated data
        /// </summary>
        private void InvalidateData()
        {
            indexPtr = null;
            textSize = Size.Empty;
            if (Invalidated != null) Invalidated(this, EventArgs.Empty);
        }

        public event EventHandler Invalidated;

        private Size fontTextureSize;

        /// <summary>
        /// Calculates the arrays for the text.
        /// </summary>
        private void CalculateData()
        {
            fontTextureSize = Font.Texture.TextureSize;
            IList<string> lines = GetLines();
            int n = SumOfChars(lines);
            indexPtr = new short[n * 6];
            vertexPtr = new short[n * 2 * 4];
            fontPtr = new float[n * 2 * 4];

            int index = 0;
            short y = 0;

            if (clipSize.Height > 0)
            {
                switch (lineAlignment)
                {
                    case Alignment.Center:
                        y = (short)((clipSize.Height - (lines.Count * Font.CharHeight * scaleFactor)) / 2);
                        break;

                    case Alignment.Far:
                        y = (short)(clipSize.Height - (lines.Count * Font.CharHeight * scaleFactor));
                        break;
                }
            }

            short charHeight = (short)(Font.CharHeight * scaleFactor);
            foreach (string line in lines)
            {
                index = CalculateLine(line, line.Length, 0, y, index);
                y += charHeight;
            }
        }

        private int SumOfChars(IEnumerable<string> lines)
        {
            int n = 0;
            foreach (var s in lines)
            {
                n += s.Length;
            }
            return n;
        }

        private IList<string> GetLines()
        {
            switch (mode & LineMode.WordWrap)
            {
                case LineMode.SingleLine:
                    return GetSingleLine();

                case LineMode.MultiLine:
                    return GetMultiLine();

                case LineMode.WordWrap:
                    return WordWrapLines();

                default:
                    throw new NotSupportedException(mode.ToString());
            }
        }

        private IList<string> GetSingleLine()
        {
            string line = text;
            int w = MeasureString(ref line, EndEllipsis);
            return new string[] { line };
        }

        private IList<string> GetMultiLine()
        {
            List<string> lines = new List<string>();
            {
                int height = this.clipSize.Height;
                if (height == 0) height = int.MaxValue;
                int h = 0;
                int ch = Font.CharHeight;
                string[] split = text.Split('\n');
                bool elipsis = this.EndEllipsis;
                foreach (string s in split)
                {
                    if ((h + ch) > height) break;
                    string line = s;
                    int w = MeasureString(ref line, elipsis);
                    lines.Add(line);
                    h += ch;
                }
                return lines;
            }
        }

        private int MeasureString(ref string s, bool elipsis)
        {
            int max = (int)(clipSize.Width / scaleFactor);
            if (max == 0f) max = int.MaxValue;
            int ellipsisMax = elipsis ? max - Font.ElipsisWidth : max;
            int width = 0;
            int elipsisLen = 0;
            for (int i = 0; i < s.Length; i++)
            {
                char c = s[i];
                int charWidth = Font.CharacterWidth(c);
                width += charWidth;
                if (width < ellipsisMax) elipsisLen = i;
                if (width > max)
                {
                    width -= charWidth;
                    s = elipsis ? s.Substring(0, elipsisLen) + "..." : s.Substring(0, elipsisLen);
                    return elipsis ? width + Font.ElipsisWidth : width;
                }
            }

            return width;
        }

        private IList<string> WordWrapLines()
        {
            List<string> lines = new List<string>();
            string text = this.text;
            int height = this.clipSize.Height;
            int h = 0;
            int ch = Font.CharHeight;
            int max = this.clipSize.Width;
            while (!string.IsNullOrEmpty(text))
            {
                if ((h + ch) > height) break;
                int n = WordWrapLine(text, max);
                lines.Add(text.Substring(0, n).TrimEnd(' ', '\t', '\r'));
                text = text.Remove(0, n).TrimStart(' ', '\t', '\r');
                h += ch;
            }
            return lines;
        }

        private int WordWrapLine(string text, int max)
        {
            int n = 0;
            int w = 0;
            int lastSpace = 0;
            int height = clipSize.Height;
            if (height == 0) height = int.MaxValue;
            int ch = Font.CharHeight;
            int h = 0;
            foreach (char c in text)
            {
                if ((h + ch) > height) break;
                if (c == '\n') return n + 1;
                w += Font.CharacterWidth(c) + characterSpace;
                n++;
                if (w > max && lastSpace > 0) return lastSpace;
                if (c == ' ' || c == '\t' || c == ',' || c == '_' || c == '\\') lastSpace = n;
            }
            return n;
        }

        private int CalculateLine(string text, int n, short x, short y, int index)
        {
            short h = (short)(Font.CharHeight * scaleFactor);
            short charSpace = (short)CharacterSpace;
            if (text != null)
            {
                int start = index;
                foreach (char c in text)
                {
                    if (--n < 0) break;
                    short charWidth = (short)(Font.CharacterWidth(c) * scaleFactor);

                    AddCharacterArrayEntry(index, x, y, (short)(x + charWidth), (short)(y + h));
                    AddIndexEntry(index);
                    CopyFontEntry(index, c);

                    index++;
                    x += (short)(charWidth + charSpace);
                }
                int w = clipSize.Width;
                if (alignment != Alignment.Near && w > 0)
                {
                    short delta = alignment == Alignment.Far ? (short)(w - x) : (short)((w - x) / 2);
                    AddAlignment(start, index, delta);
                }
            }
            return index;
        }

        private void AddAlignment(int start, int index, short offset)
        {
            short[] a = vertexPtr;
            for (int i = start; i < index; i++)
            {
                int aIndex = i * 8;

                a[aIndex + 0] += offset;
                a[aIndex + 2] += offset;
                a[aIndex + 4] += offset;
                a[aIndex + 6] += offset;
            }
        }

        private int GetMaxChars(string text)
        {
            return GetMaxChars(text, clipSize.Width);
        }

        private int GetMaxChars(string text, int width)
        {
            if (string.IsNullOrEmpty(text)) return 0;

            int max = (int)(width / scaleFactor);
            if (max == 0f) return text.Length;
            int n = 0;
            int w = -1; // must not be null, otherwhise a MeasureString would return a width that does NOT fit in all the charactors!
            foreach (char c in text)
            {
                int charWidth = Font.CharacterWidth(c);
                w += charWidth;
                if (w > max) break;
                n++;
            }
            int l = text.Length;
            return n < l ? n : l;
        }

        private void AddIndexEntry(int index)
        {
            short stripIndex = (short)(index * 4);
            int ixPos = index * 6;
            indexPtr[ixPos + 0] = stripIndex;
            indexPtr[ixPos + 1] = (short)(stripIndex + 1);
            indexPtr[ixPos + 2] = (short)(stripIndex + 2);
            indexPtr[ixPos + 3] = (short)(stripIndex + 2);
            indexPtr[ixPos + 4] = (short)(stripIndex + 1);
            indexPtr[ixPos + 5] = (short)(stripIndex + 3);
        }

        private void CopyFontEntry(int pos, char c)
        {
            int charIndex = Font.GetCharIndex(c);
            pos *= 8;
            charIndex *= 3;
            short[] srcPtr = Font.TextureArrayPointer;

            int x = srcPtr[charIndex + 0];
            int y = srcPtr[charIndex + 1];
            int cw = srcPtr[charIndex + 2];
            int ch = Font.CharHeight;

            float w = fontTextureSize.Width;
            float h = fontTextureSize.Height - 1;

            float left = (float)x / w;
            float top = (float)y / h;
            float right = (float)(x + cw) / w;
            float bottom = (float)(y + ch) / h;

            float[] a = fontPtr;
            a[pos + 0] = left;
            a[pos + 1] = bottom;

            a[pos + 2] = left;
            a[pos + 3] = top;

            a[pos + 4] = right;
            a[pos + 5] = bottom;

            a[pos + 6] = right;
            a[pos + 7] = top;
        }


        private void AddCharacterArrayEntry(int index, short left, short top, short right, short bottom)
        {
            int aIndex = index * 8;

            short[] a = vertexPtr;
            a[aIndex + 0] = left;
            a[aIndex + 1] = bottom;

            a[aIndex + 2] = left;
            a[aIndex + 3] = top;

            a[aIndex + 4] = right;
            a[aIndex + 5] = bottom;

            a[aIndex + 6] = right;
            a[aIndex + 7] = top;
        }

        private Size MeasureSize()
        {
            IList<string> lines = GetLines();
            int h = lines.Count * Font.CharHeight;
            int width = 0;
            foreach (string line in lines)
            {
                string s = line;
                int w = MeasureString(ref s, false);
                if (width < w) width = w;
            }
            return new Size((int)(width * scaleFactor + 0.5f), (int)(h * scaleFactor + 0.5f));
        }


        public void PreLoad()
        {
            if (indexPtr == null) CalculateData();
        }

        /// <summary>
        /// Renders the <paramref name="Text"/> property at the current matrix view location.
        /// </summary>
        public void Render()
        {
            PreLoad();

            if (!Color.IsEmpty)
            {
                gl.Color4x(Color.R << 8, Color.G << 8, Color.B << 8, Color.A << 8);
            }
            //gl.Enable(Capability.Blend);
            gl.BindTexture(TargetTexture.Texture2D, Font.Texture.Name);
            //gl.ActiveTexture(TextureUnit.Texture0);
            //gl.ClientActiveTexture(TextureUnit.Texture0);
            //gl.EnableClientState(ClientStateArray.TextureCoord);
            //gl.Enable(Capability.Texture2D);
//            gl.EnableClientState(ClientStateArray.Vertex);
            gl.TexCoordPointer(2, glValueType.Float, 0, this.fontPtr);
            gl.VertexPointer(2, glValueType.Short, 0, vertexPtr);
            gl.DrawElements(PrimitiveType.Triangles, indexPtr.Length, glValueType.UnsignedShort, indexPtr);

        }

        /// <summary>
        /// Renders the specified string and sets this string as current <paramref name="Text"/> property. 
        /// After rendering, the current matrix view is transfered by one character row.
        /// </summary>
        /// <param name="text">The Text to render.</param>
        public void Render(string text)
        {
            Text = text;
            Render();
            gl.Translatef(0f, TextSize.Height, 0f);
        }

        #endregion
    }
}
